using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using FriendsNearby.Services.Entities;
using InSTEDD.Labs.Geo;
using Microsoft.Practices.EnterpriseLibrary.PolicyInjection;
using Microsoft.Practices.EnterpriseLibrary.Logging;

namespace FriendsNearby.Services
{
    public class PeopleNearbyService : IPeopleNearbyService
    {
        ILocationService locationService;
        IUsersService usersService;
        ISocialNetworkService socialNetworkService;

        public PeopleNearbyService(ILocationService locationService, IUsersService usersService, ISocialNetworkService socialNetworkService)
        {
            this.locationService = locationService;
            this.usersService = usersService;
            this.socialNetworkService = socialNetworkService;
        }

        /// <summary>
        /// Report of <see cref="IPeopleNearbyService.GetPeopleNearby"/>
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="countFriendsNearby"></param>
        /// <param name="nearestDistance"></param>
        public PeopleNearbySummary GetPeopleNearbySummary(Guid userID)
        {
            IEnumerable<PersonNearby> personNearby = GetContacts(userID, QueryContactsKind.Nearby);
            int countFriendsNearby = 0;
			double? nearestDistance = null;
            foreach (PersonNearby pn in personNearby)
            {
                nearestDistance = Math.Min(nearestDistance.GetValueOrDefault(double.MaxValue), pn.Distance.GetValueOrDefault(double.MaxValue));
                countFriendsNearby++;
            }

			return new PeopleNearbySummary(countFriendsNearby, nearestDistance);
        }

        private IList<PersonNearby> GetPeopleNearby(Guid userID)
        {
            LocationInfo info = locationService.GetCurrentLocation(userID);

            return GetPeopleNearby(info, null);
        }

        private IList<PersonNearby> GetAllPeople(Guid userID)
        {
            LocationInfo info = locationService.GetCurrentLocation(userID);

            return InternalGetPeopleNearby(userID, info, null, null, true);
        }

        private IList<PersonNearby> GetPeopleNearby(Guid userID, DateTime pointInTime)
        {
            //By definition, none was near none in the beginning of the beginnings
            if (pointInTime == DateTime.MinValue)
                return default(IList<PersonNearby>);

            LocationInfo info = locationService.GetLocationAt(userID, pointInTime);

            return GetPeopleNearby(info, pointInTime);
        }

        private IList<PersonNearby> GetAllFriends(Guid userID)
        {
            LocationInfo info = locationService.GetCurrentLocation(userID);
			return InternalGetPeopleNearby(userID, info, null, null, false);
        }
        
        public IList<string> GetNotificationsForUser(Guid userID)
        {
            try
            {

                IList<PersonNearby> friendsNearbyNow = GetPeopleNearby(userID);
                Logger.Write(string.Format("FacebookID:{0} has {1} friends nearby now", userID, friendsNearbyNow.Count));

                IList<PersonNearby> friendsNearbyBefore = default(IList<PersonNearby>); 
                
                DateTime lastNotification = locationService.GetLastNotificationDate(userID);
                //if this is not the first notification to the user                
                if (lastNotification != DateTime.MinValue)
                {
                    friendsNearbyBefore = GetPeopleNearby(userID, lastNotification);
                    Logger.Write(string.Format("UserID:{0} had {1} friends nearby at the time of the last notification: {2}",
                                                userID, friendsNearbyBefore.Count, lastNotification));
                                      
                }

                return GetNewFriendsNearby(friendsNearbyBefore, friendsNearbyNow); 
            }
            catch (Exception ex)
            {
                Logger.Write(string.Format("Exception thrown in GetNotificationsForFacebookUser:{0}",
                                            ex.ToString()));

                return new List<string>();
            }

        }

		public static IList<string> GetNewFriendsNearby(IEnumerable<PersonNearby> friendsNearbyBefore, IEnumerable<PersonNearby> friendsNearbyNow)
		{
			var results = new List<string>();

			foreach (PersonNearby personNearbyNow in friendsNearbyNow)
			{
				bool alreadyNearby = false;
				if (friendsNearbyBefore != null)
				{
					foreach (PersonNearby userNearbyBefore in friendsNearbyBefore)
					{
						if (personNearbyNow.UserID == userNearbyBefore.UserID)
						{
							alreadyNearby = true;
							break;
						}
					}
				}
				if (!alreadyNearby)
					results.Add(personNearbyNow.DisplayName);
			}
			return results;
		}

        private List<PersonNearby> GetPeopleNearby(LocationInfo info, DateTime? pointInTime)
        {
            if (info == null || info.Location == null)
            {
                return new List<PersonNearby>();
            }

			UserRegistry preferences = usersService.GetUserPreferences(info.UserID);

			return InternalGetPeopleNearby(info.UserID, info, (double)preferences.NearbyRadiusKM, pointInTime, true);
        }

        /// <summary>
        /// List people nearby the user.
        /// Nearby means less or equal than <paramref name="distance"/> (if not null).
        /// <paramref name="includeFriendsOfFriends" /> specify if friends of friends should be computed
        /// </summary>
        /// <param name="userInfo"></param>
        /// <param name="distance"></param>
        /// <param name="pointInTime"></param>
        /// <param name="includeFriendsOfFriends"></param>
        /// <returns></returns>
        private List<PersonNearby> InternalGetPeopleNearby(Guid userID, LocationInfo userInfo, double? distance, DateTime? pointInTime, bool includeFriendsOfFriends)
        {
            List<PersonNearby> result = new List<PersonNearby>();

            IEnumerable<Guid> users;

            //  Add Friends
			users = socialNetworkService.GetFriends(userID);
            result.AddRange(CreatePersonNearbies(userInfo, users, PersonNearbyRelation.Friend, distance, pointInTime));

            if (includeFriendsOfFriends)
            {
                //  Add Friends of Friends
				users = socialNetworkService.GetFriendsOfFriends(userID);
                result.AddRange(CreatePersonNearbies(userInfo, users, PersonNearbyRelation.FriendOfAFriend, distance, pointInTime));
            }

            // We have to sort again.
            // TODO optimize with another algorithm with merging.
            // Sort by distance
            #region sort by distance

            SortByDistanceAndFacebookUserId(result,
                x => x.Distance);
            #endregion

            return result;
        }

        private IEnumerable<PersonNearby> CreatePersonNearbies(LocationInfo locationInfo, IEnumerable<Guid> users, PersonNearbyRelation relation, double? distance, DateTime? pointInTime)
        {
            List<PersonNearby> personsNearby = new List<PersonNearby>();
            PersonNearbyBuilder personNearbyBuilder = new PersonNearbyBuilder("Unknown {0}");

            // users must be sorted before going to personNearbyBuilder and get numbered and Unkown {0}
            // that way, the name assigned is the same, no matter the distance.
            List<LocationInfo> friendsInfo = new List<LocationInfo>(GetUsersNearby(locationInfo.Location, users, distance, pointInTime));
            SortByDistanceAndFacebookUserId(friendsInfo,
                x => locationInfo.Location == null ? null : (double?)LatLong.DistanceInKm(locationInfo.Location, x.Location));

            foreach (LocationInfo friendInfo in friendsInfo)
            {
                try
                {
					UserRegistry preferences = usersService.GetUserPreferences(friendInfo.UserID);
                    string friendDisplayName;

					friendDisplayName = preferences.DisplayName;

                    PersonNearby person = personNearbyBuilder.CreateFriendNearby(locationInfo, friendInfo, friendDisplayName, preferences, relation);

                    if (person != null)
                        personsNearby.Add(person);
                }
                catch (Exception e)
                {
                    LogEntry logEntry = new LogEntry();
                    logEntry.Message = e.ToString();
                    logEntry.Categories.Add("Oddities");
                    logEntry.EventId = LogEventIds.UnableToCreatePersonNearby;

                    Logger.Write(logEntry);
                }
            }
            return personsNearby;
        }

        private void SortByDistanceAndFacebookUserId<T>(List<T> items, Func<T, double?> distance)
        {
            items.Sort(
                (item1, item2) =>
                {
                    var distance1 = distance(item1).GetValueOrDefault(double.MaxValue);
                    var distance2 = distance(item2).GetValueOrDefault(double.MaxValue);
					return distance1.CompareTo(distance2);
                });
        }

        private IEnumerable<LocationInfo> GetUsersNearby(LatLong myLocation, IEnumerable<Guid> userIds, double? distance, DateTime? pointInTime)
        {
            foreach (Guid user in userIds)
            {
                LocationInfo info;

                if(pointInTime.HasValue)
                    info = locationService.GetLocationAt(user, pointInTime.Value);
                else
                    info = locationService.GetCurrentLocation(user);

                if (info != null && info.Location != null && IsNear(myLocation, info.Location, distance)) 
                    yield return info;
            }
        }

        private static bool IsNear(LatLong fromLocation, LatLong toLocation, double? distance)
        {
            return (distance == null || LatLong.DistanceInKm(fromLocation, toLocation) <= distance);
        }

        public IList<PersonNearby> GetContacts(Guid userID, QueryContactsKind kind)
        {
            switch (kind)
            {
                case QueryContactsKind.Nearby:
                    return GetPeopleNearby(userID);
                case QueryContactsKind.Direct:
                    return GetAllFriends(userID);
                case QueryContactsKind.Any:
                    return GetAllPeople(userID);
                default:
                    throw new NotSupportedException();
            }
        }
    }
}
